﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace BibtexParser
{
	class Program
	{
		static void Main(string[] args)
		{
			var baseBib = args.Length > 0 ? args[0] : "jis.bib";
			var helperBib = args.Length > 1 ? args[1] : "bib.bib";
			var outputBib = baseBib + ".out";

			var baseBibContent = ReadBib(baseBib);
			var helperBibContent = ReadBib(helperBib);
			var baseBibDat = BibtexParser.ExtractBibs(baseBibContent);
			var helperBibDat = BibtexParser.ExtractBibs(helperBibContent).ToList();

			var joinLog = File.CreateText(baseBib + ".csv");
			var bibJoin = baseBibDat.Select(bb => MergeBibs(bb, helperBibDat, joinLog));

			var outFile = File.CreateText(outputBib);
			foreach (var bib in bibJoin)
			{
				outFile.WriteLine(bib.ToString());
			}
			joinLog.Close();
			outFile.Close();
		}

		private static Bib MergeBibs(Bib bb, IList<Bib> helperBibDat, StreamWriter log)
		{
			var bbTitle = bb.Properties.Where(pv => pv.Key.Equals("title", StringComparison.InvariantCultureIgnoreCase))
				.Select(kv => kv.Value)
				.FirstOrDefault();
			if (bbTitle == null)
				ErrorHappened(bb.Id);
			var joinedHelpers = helperBibDat
					.SelectMany(
									pv =>
									pv.Properties
										.Where(p => p.Key.Equals("title", StringComparison.InvariantCultureIgnoreCase))
										.Select(
												p => new { Rank = MatchRank(bbTitle, p.Value.ToLowerInvariant()), HelperBib = pv })
										.Where(rt => !double.IsPositiveInfinity(rt.Rank) && rt.Rank < 10))
					.ToList();
			if (!joinedHelpers.Any())
			{
				log.WriteLine("{0},{1},{2},{3}", bb.Id, 1, bb.Title.Replace(",", ";"), bb.Title.Replace(",", ";"));
				return bb;
			}
			var minRank = joinedHelpers.Min(jh => jh.Rank);
			var helperPeer = joinedHelpers.First(jh => jh.Rank.Equals(minRank)).HelperBib;
			helperBibDat.Remove(helperPeer);
			var rv = new Bib
						{
							Id = bb.Id,
							Type = helperPeer.Type,
						};
			foreach (var prop in helperPeer.Properties)
				rv.Properties.Add(prop);
			log.WriteLine("{0},{1},{2},{3}", bb.Id, 0, bb.Title.Replace(",", ";"), rv.Title.Replace(",", ";"));
			return rv;
		}

		private static double MatchRank(string leftTitle, string rightTitle)
		{
			var leftWords = leftTitle.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).OrderBy(s => s).ToList();
			var rightWords = rightTitle.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).OrderBy(s => s).ToList();
			if (leftWords.Count < rightWords.Count)
			{
				var t = leftWords;
				leftWords = rightWords;
				rightWords = t;
			}
			if (Math.Abs(leftWords.Count - rightWords.Count) > 5)
				return double.PositiveInfinity;
			var ranks =
				leftWords
				.Select(lw =>
							{
								var dists = rightWords
									.Select(rw => new { item = rw, lev = LevenshteinDistance.Compute(lw, rw) })
									.ToList();
								if (dists.Count == 0)
									return lw.Length;
								var minDist = dists.Min(v => v.lev);
								var item = dists.First(d => d.lev == minDist).item;
								rightWords.Remove(item);
								return minDist;
							});
			var factor = 100d/Math.Min(leftTitle.Length, rightTitle.Length);
			var rank = ranks.Sum(r => Math.Sqrt(r*factor));
			if (rank > leftWords.Count * 3)
				return double.PositiveInfinity;
			return rank;
		}

		private static void ErrorHappened(string id)
		{
			Console.WriteLine("Bib with id {0} does not contain a title!", id);
		}

		private static string ReadBib(string baseBib)
		{
			var contentLines = File.ReadAllLines(baseBib)
				.Where(l => !l.StartsWith("%%"))
				.Select(StripComments);
			var content = string.Join("", contentLines);
			return content;
		}

		private static string StripComments(string s)
		{
			var parts = s.Split(new[] { "%%" }, 2, StringSplitOptions.None);
			if (parts.Length > 0)
				return parts[0];
			return s;
		}
	}
}
